package com.lauor.smpdb.pool;

import com.lauor.smpdb.SmpConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
 * @Function:数据连接池
 */
public class SmpDbPool {
    private final Logger LOG = LoggerFactory.getLogger(SmpDbPool.class);
    //数据库连接队列
    private BlockingQueue<SmpConnectionProxy> dbConnectQueue;

    private SmpConfig smpConfig;
    //已创建数据库连接数量,读多写少，cas法
    private volatile AtomicInteger createdDbConnectionAmount = new AtomicInteger(0);

    private Executor netTimeoutExecutor;

    private final Lock CONNECTION_LOCK = new ReentrantLock();
    //是否启用主动关闭空闲连接逻辑
    private boolean releaseLeisureActive;
    //最近执行关闭空闲连接时间
    private volatile long lastReleaseLeisureMillis;

    public SmpDbPool(SmpConfig config) {
        this.smpConfig = config;
        this.dbConnectQueue = new ArrayBlockingQueue<>( smpConfig.getMaxActive() );
        this.netTimeoutExecutor = command -> command.run();
    }

    public void initPool() throws SQLException {
        this.releaseLeisureActive = this.smpConfig.getMaxActive() > this.smpConfig.getMinIdle();

        for (int i = 0; i < this.smpConfig.getMinIdle(); i++) {
            try {
                SmpConnectionProxy connection = this.createConnection();
                createdDbConnectionAmount.addAndGet(1);
                this.dbConnectQueue.add(connection);
            } catch (IllegalStateException ex){
                LOG.error("add database connection to pool error", ex);
            } catch (ClassNotFoundException ex){
                LOG.error("class %s not found", this.smpConfig.getDriverClassName(), ex);
                throw new SQLException(ex);
            }
        }
        LOG.debug("SmpDbPool initialized");
    }

    private SmpConnectionProxy createConnection() throws ClassNotFoundException, SQLException {
        Class.forName( this.smpConfig.getDriverClassName() );
        Connection connection = DriverManager.getConnection( this.smpConfig.getUrl(), this.smpConfig.getUsername(), this.smpConfig.getPassword());
        LOG.debug("create new database connection with url:{}", this.smpConfig.getUrl());
        return new SmpConnectionProxy(connection, this, connection.getAutoCommit());
    }

    public Connection borrowConnection() throws Exception {
        boolean isDebugEnable = LOG.isDebugEnabled();
        long sTime = 0;
        if (isDebugEnable){//尽量少访问系统资源
            sTime = System.currentTimeMillis();
        }
        try {
            SmpConnectionProxy connection = this.getConnectionFromQueue();
            if (connection != null) return connection;

            //未获取到有效连接,校验是否创建连接
            int maxActive = this.smpConfig.getMaxActive(), dbAmount = createdDbConnectionAmount.get();
            if (dbAmount >= maxActive){
                //尝试等待其他线程释放连接
                connection = this.dbConnectQueue.poll(this.smpConfig.getMaxWaitMillis(), TimeUnit.MILLISECONDS);
                if (connection != null) return connection.borrowConnection();

                LOG.error("get connection error, no connection available,maxActive:{}, activated connection:{}", maxActive, dbAmount);
                throw new IllegalStateException("database connection pool too busy");
            }
            connection = this.createConnectionForPool();
            if (connection != null) return connection;

            throw new IllegalStateException("database connection pool too busy");
        } finally {
            if (isDebugEnable){
                LOG.debug("got database connection cost {} ms", System.currentTimeMillis() - sTime);
            }
        }
    }
    //从队列获取连接
    private SmpConnectionProxy getConnectionFromQueue() throws SQLException {
        int times = 0;
        int maxConnections = this.smpConfig.getMaxActive();
        while (times++ < maxConnections){
            SmpConnectionProxy connection = this.dbConnectQueue.poll();
            if (connection == null) return null;

            if (connection.isValid()) return connection.borrowConnection();

            //destroy invalid connection
            connection.destroy();
            createdDbConnectionAmount.decrementAndGet();
            LOG.info("destroyed invalid jdbc connection");
        }
        return null;
    }

    private SmpConnectionProxy createConnectionForPool() throws Exception{
        boolean locked = false;
        try {
            locked = CONNECTION_LOCK.tryLock(this.smpConfig.getMaxWaitMillis() >> 1, TimeUnit.MILLISECONDS);
            if (!locked){
                LOG.error("server busy! getting lock to create connection error!");
                return null;
            }
            SmpConnectionProxy connection = this.getConnectionFromQueue();
            if (connection != null) return connection;

            int dbAmount = createdDbConnectionAmount.get(), maxActive = this.smpConfig.getMaxActive();
            if (dbAmount >= maxActive){
                LOG.error("get connection error, no connection available,maxActive:{}, activated connection:{}", maxActive, dbAmount);
                return null;
            }
            connection = this.createConnection();
            createdDbConnectionAmount.addAndGet(1);
            return connection.borrowConnection();
        } catch (ClassNotFoundException ex){
            LOG.error("class {} not found", this.smpConfig.getDriverClassName(), ex);
            throw new SQLException(ex);
        }finally {
            if (locked){
                CONNECTION_LOCK.unlock();
            }
        }
    }

    public void releaseConnection(SmpConnectionProxy connection){
        boolean isDebugEnable = LOG.isDebugEnabled();
        long sTime = 0;
        if (isDebugEnable) {
            sTime = System.currentTimeMillis();
        }
        try {
            this.dbConnectQueue.add(connection);
            LOG.debug("return jdbc connection");
            //尝试销毁多余空闲连接
            tryReleaseLeiSureConnections();
        } catch (IllegalStateException e) {
            LOG.error("releaseConnection error", e);
        } finally {
            if (isDebugEnable){
                LOG.debug("releasing database connection cost {} ms", System.currentTimeMillis() - sTime);
            }
        }
    }
    /**
     * @description:尝试回收空闲连接
     * @return void
     */
    private void tryReleaseLeiSureConnections(){
        if (!releaseLeisureActive) return;
        //时间不够
        long nowMillis = System.currentTimeMillis();
        boolean overTime = nowMillis - lastReleaseLeisureMillis >= smpConfig.getTimeBetweenEvictionRunsMillis();
        lastReleaseLeisureMillis = nowMillis;
        if (!overTime) return;

        //获取连接数
        int minIdle = smpConfig.getMinIdle();
        //无需销毁
        if (dbConnectQueue.size() <= minIdle) return;

        //先尝试获取锁，便于阻塞创建数据库连接逻辑
        boolean locked = false;
        try {
            locked = CONNECTION_LOCK.tryLock(2, TimeUnit.SECONDS);
            if (!locked){
                throw new Exception("server busy! getting lock to destroy leisure connection timeout!");
            }
            //销毁数量
            int destroyAmount = dbConnectQueue.size() - minIdle;
            if (destroyAmount <= 0) return;
            //开始销毁
            for (int i = 0; i < destroyAmount; i++) {
                SmpConnectionProxy temp = dbConnectQueue.poll();
                if (temp == null) continue;
                temp.destroy();
                //创建数减1
                createdDbConnectionAmount.decrementAndGet();
            }
        } catch (Exception ex){
            LOG.error("", ex);
        } finally {
            if (locked){
                CONNECTION_LOCK.unlock();
            }
        }
    }

    public int getConnectionNum(){
        return this.createdDbConnectionAmount.get();
    }

    public int getAvailableConnectionNum(){
        return this.dbConnectQueue.size();
    }

    public SmpConfig getConfig(){
        return this.smpConfig;
    }

    Executor getNetTimeoutExecutor(){
        return this.netTimeoutExecutor;
    }
}